TypeScript jenerikleri üzerine kapsamlı bir rehber; sözdizimi, faydaları, ileri düzey kullanımı ve küresel yazılım geliştirmede karmaşık veri tiplerini ele alma.
TypeScript Generics: Sağlam Uygulamalar için Karmaşık Veri Tiplerinde Uzmanlaşma
JavaScript'in bir üst kümesi olan TypeScript, geliştiricilere statik tipleme yoluyla daha sağlam ve sürdürülebilir kod yazma gücü verir. En güçlü özelliklerinden biri olan jenerikler (generics), tür güvenliğini korurken çeşitli veri tipleriyle çalışabilen kodlar yazmanıza olanak tanır. Bu rehber, TypeScript jeneriklerini küresel yazılım geliştirme bağlamında karmaşık veri tiplerine uygulanmasına odaklanarak kapsamlı bir şekilde incelemektedir.
Jenerikler (Generics) Nedir?
Jenerikler, farklı türlerle çalışabilen yeniden kullanılabilir kod yazmanın bir yolunu sunar. Desteklemek istediğiniz her tür için ayrı fonksiyonlar veya sınıflar yazmak yerine, tür parametreleri kullanan tek bir fonksiyon veya sınıf yazabilirsiniz. Bu tür parametreleri, fonksiyon veya sınıf çağrıldığında veya örneklendiğinde kullanılacak olan gerçek türler için birer yer tutucudur. Bu, özellikle bu yapıların içindeki veri türünün değişebileceği karmaşık veri yapılarıyla uğraşırken kullanışlıdır.
Jenerik Kullanmanın Faydaları
- Kodun Yeniden Kullanılabilirliği: Kodu bir kez yazın ve farklı türlerle kullanın. Bu, kod tekrarını azaltır ve kod tabanınızı daha sürdürülebilir hale getirir.
- Tür Güvenliği: Jenerikler, TypeScript derleyicisinin derleme zamanında tür güvenliğini zorunlu kılmasını sağlar. Bu, tür uyuşmazlıklarıyla ilgili çalışma zamanı hatalarını önlemeye yardımcı olur.
- Geliştirilmiş Okunabilirlik: Jenerikler, fonksiyonlarınızın ve sınıflarınızın hangi türlerle çalışmak üzere tasarlandığını açıkça belirterek kodunuzu daha okunabilir hale getirir.
- Artırılmış Performans: Bazı durumlarda, jenerikler performans iyileştirmelerine yol açabilir çünkü derleyici, kullanılan belirli türlere göre oluşturulan kodu optimize edebilir.
Temel Jenerik Sözdizimi
Temel jenerik sözdizimi, tür parametrelerini bildirmek için açılı parantezlerin (< >) kullanılmasını içerir. Bu tür parametreleri genellikle T
, K
, V
vb. olarak adlandırılır, ancak herhangi bir geçerli tanımlayıcıyı kullanabilirsiniz. İşte basit bir jenerik fonksiyon örneği:
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);
console.log(myString); // Çıktı: hello
console.log(myNumber); // Çıktı: 123
console.log(myBoolean); // Çıktı: true
Bu örnekte, <T>
, T
adında bir tür parametresi bildirir. identity
fonksiyonu, T
türünde bir argüman alır ve T
türünde bir değer döndürür. Fonksiyonu çağırırken, tür parametresini açıkça belirtebilir (örneğin, identity<string>
) veya TypeScript'in argüman türüne göre çıkarım yapmasına izin verebilirsiniz.
Karmaşık Veri Tipleriyle Çalışma
Jenerikler, diziler, nesneler ve arayüzler gibi karmaşık veri tipleriyle uğraşırken özellikle değerli hale gelir. Bazı yaygın senaryoları inceleyelim:
Jenerik Diziler
Farklı türlerdeki dizilerle çalışan fonksiyonlar veya sınıflar oluşturmak için jenerikleri kullanabilirsiniz:
function arrayToString<T>(arr: T[]): string {
return arr.join(", ");
}
let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];
console.log(arrayToString(numberArray)); // Çıktı: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Çıktı: apple, banana, cherry
Burada, arrayToString
fonksiyonu T[]
türünde bir dizi alır ve dizinin bir dize temsilini döndürür. Bu fonksiyon her türden diziyle çalışır, bu da onu oldukça yeniden kullanılabilir kılar.
Jenerik Nesneler
Jenerikler ayrıca farklı şekillerdeki nesnelerle çalışan fonksiyonları veya sınıfları tanımlamak için de kullanılabilir:
interface Person {
name: string;
age: number;
country: string; // Küresel bağlam için ülke eklendi
}
interface Product {
id: number;
name: string;
price: number;
currency: string; // Küresel bağlam için para birimi eklendi
}
function displayInfo<T extends { name: string }>(item: T): void {
console.log(`Name: ${item.name}`);
}
let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };
displayInfo(person); // Çıktı: Name: Alice
displayInfo(product); // Çıktı: Name: Laptop
Bu örnekte, displayInfo
fonksiyonu, string türünde bir name
özelliğine sahip olması gereken T
türünde bir nesne alır. extends { name: string }
ifadesi, T
tür parametresi için minimum gereksinimleri belirten bir kısıtlamadır (constraint). Bu, fonksiyonun name
özelliğine güvenli bir şekilde erişebilmesini sağlar.
İleri Düzey Jenerik Kullanımı
TypeScript jenerikleri, daha da esnek ve güçlü kod oluşturmanıza olanak tanıyan daha gelişmiş özellikler sunar. Bu özelliklerden bazılarını inceleyelim:
Çoklu Tür Parametreleri
Birden çok tür parametresine sahip fonksiyonlar veya sınıflar tanımlayabilirsiniz:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
interface Name {
firstName: string;
}
interface Age {
age: number;
}
const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };
const merged = merge(person, details);
console.log(merged.firstName); // Çıktı: Bob
console.log(merged.age); // Çıktı: 42
merge
fonksiyonu, T
ve U
türlerinde iki nesne alır ve her iki nesnenin özelliklerini içeren yeni bir nesne döndürür. Bu, farklı kaynaklardan gelen verileri birleştirmenin güçlü bir yoludur.
Jenerik Kısıtlamalar
Daha önce gösterildiği gibi, kısıtlamalar bir jenerik tür parametresiyle kullanılabilecek türleri sınırlamanıza olanak tanır. Bu, jenerik kodun belirtilen türler üzerinde güvenli bir şekilde çalışabilmesini sağlar.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity([1, 2, 3]); // Çıktı: 3
loggingIdentity("hello"); // Çıktı: 5
// loggingIdentity(123); // Hata: 'number' türündeki argüman, 'Lengthwise' türündeki parametreye atanamaz.
loggingIdentity
fonksiyonu, number türünde bir length
özelliğine sahip olması gereken T
türünde bir argüman alır. Bu, fonksiyonun length
özelliğine güvenli bir şekilde erişebilmesini sağlar.
Jenerik Sınıflar
Jenerikler sınıflarla da kullanılabilir:
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data = this.data.filter(d => d !== item);
}
getItems(): T[] {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Çıktı: [ 'banana' ]
const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Çıktı: [ 2 ]
DataStorage
sınıfı, herhangi bir T
türünde veri depolayabilir. Bu, tür güvenliğine sahip yeniden kullanılabilir veri yapıları oluşturmanıza olanak tanır.
Jenerik Arayüzler
Jenerik arayüzler, farklı türlerle çalışabilen sözleşmeler tanımlamak için kullanışlıdır. Örneğin:
interface Result<T, E> {
success: boolean;
data?: T;
error?: E;
}
interface User {
id: number;
username: string;
email: string;
}
interface ErrorMessage {
code: number;
message: string;
}
function fetchUser(id: number): Result<User, ErrorMessage> {
if (id === 1) {
return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
} else {
return { success: false, error: { code: 404, message: "Kullanıcı bulunamadı" } };
}
}
const userResult = fetchUser(1);
if (userResult.success) {
console.log(userResult.data.username);
} else {
console.log(userResult.error.message);
}
Result
arayüzü, bir işlemin sonucunu temsil etmek için jenerik bir yapı tanımlar. Ya T
türünde veri ya da E
türünde bir hata içerebilir. Bu, asenkron işlemleri veya başarısız olabilecek işlemleri yönetmek için yaygın bir kalıptır.
Yardımcı Türler ve Jenerikler
TypeScript, jeneriklerle iyi çalışan birkaç yerleşik yardımcı tür sağlar. Bu yardımcı türler, türleri güçlü yollarla dönüştürmenize ve değiştirmenize yardımcı olabilir.
Partial<T>
Partial<T>
, T
türünün tüm özelliklerini isteğe bağlı yapar:
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
const partialPerson: PartialPerson = { name: "Alice" }; // Geçerli
Readonly<T>
Readonly<T>
, T
türünün tüm özelliklerini salt okunur yapar:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Hata: 'age' salt okunur bir özellik olduğu için atama yapılamaz.
Pick<T, K>
Pick<T, K>
, T
türünden bir dizi K
özelliği seçer:
interface Person {
name: string;
age: number;
email: string;
}
type NameAndAge = Pick<Person, "name" | "age">;
const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };
Omit<T, K>
Omit<T, K>
, T
türünden bir dizi K
özelliğini kaldırır:
interface Person {
name: string;
age: number;
email: string;
}
type PersonWithoutEmail = Omit<Person, "email">;
const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };
Record<K, T>
Record<K, T>
, anahtarları K
ve değerleri T
türünde olan bir tür oluşturur:
type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Küresel bağlam için genişletilmiş liste
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Küresel bağlam için genişletilmiş liste
type CurrencyMap = Record<CountryCodes, Currency>;
const currencyMap: CurrencyMap = {
"US": "USD",
"CA": "CAD",
"UK": "GBP",
"DE": "EUR",
"FR": "EUR",
"JP": "JPY",
"CN": "CNY",
"IN": "INR",
"BR": "BRL",
"AU": "AUD",
};
Eşlenmiş Tipler (Mapped Types)
Eşlenmiş tipler, mevcut türleri özelliklerini yineleyerek dönüştürmenize olanak tanır. Bu, mevcut olanlara dayalı yeni türler oluşturmanın güçlü bir yoludur. Örneğin, başka bir türün tüm özelliklerini salt okunur yapan bir tür oluşturabilirsiniz:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
};
const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Hata: 'age' salt okunur bir özellik olduğu için atama yapılamaz.
Bu örnekte, [K in keyof Person]
, Person
arayüzünün tüm anahtarları üzerinde yinelenir ve Person[K]
her bir özelliğin türüne erişir. readonly
anahtar kelimesi her özelliği salt okunur yapar.
Koşullu Tipler (Conditional Types)
Koşullu tipler, koşullara göre tipler tanımlamanıza olanak tanır. Bu, farklı senaryolara uyum sağlayan tipler oluşturmanın güçlü bir yoludur.
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string
function getValue<T>(value: T): NonNullable<T> {
if (value == null) { // Hem null hem de undefined durumlarını ele alır
throw new Error("Değer null veya undefined olamaz");
}
return value as NonNullable<T>;
}
try {
const validValue = getValue("hello");
console.log(validValue.toUpperCase()); // Çıktı: HELLO
const invalidValue = getValue(null); // Bu bir hata fırlatacaktır
console.log(invalidValue); // Bu satıra ulaşılamayacak
} catch (error: any) {
console.error(error.message); // Çıktı: Değer null veya undefined olamaz
}
Bu örnekte, NonNullable<T>
türü, T
'nin null
veya undefined
olup olmadığını kontrol eder. Eğer öyleyse, türün izin verilmediği anlamına gelen never
döndürür. Aksi takdirde, T
döndürür. Bu, null olamayacağı garanti edilen türler oluşturmanıza olanak tanır.
Jenerik Kullanımı için En İyi Uygulamalar
Jenerikleri kullanırken aklınızda bulundurmanız gereken bazı en iyi uygulamalar şunlardır:
- Açıklayıcı tür parametresi adları kullanın: Tür parametresinin amacını açıkça belirten adlar seçin.
- Bir jenerik tür parametresiyle kullanılabilecek türleri sınırlamak için kısıtlamalar kullanın: Bu, jenerik kodunuzun belirtilen türler üzerinde güvenli bir şekilde çalışabilmesini sağlar.
- Jenerik kodunuzu basit ve odaklı tutun: Jenerik kodunuzu çok fazla tür parametresi veya karmaşık kısıtlamalarla aşırı karmaşık hale getirmekten kaçının.
- Jenerik kodunuzu ayrıntılı bir şekilde belgeleyin: Tür parametrelerinin amacını ve kullanılan kısıtlamaları açıklayın.
- Kodun yeniden kullanılabilirliği ve tür güvenliği arasındaki dengeyi göz önünde bulundurun: Jenerikler kodun yeniden kullanılabilirliğini artırabilirken, kodunuzu daha karmaşık hale de getirebilirler. Jenerikleri kullanmadan önce faydalarını ve dezavantajlarını tartın.
- Yerelleştirme ve küreselleştirmeyi (l10n ve g11n) göz önünde bulundurun: Farklı bölgelerdeki kullanıcılara gösterilmesi gereken verilerle çalışırken, jeneriklerinizin uygun biçimlendirme ve kültürel gelenekleri desteklediğinden emin olun. Örneğin, sayı ve tarih biçimlendirmesi yerel ayarlara göre önemli ölçüde değişebilir.
Küresel Bağlamda Örnekler
Jeneriklerin küresel bir bağlamda nasıl kullanılabileceğine dair bazı örneklere bakalım:
Para Birimi Dönüşümü
interface ConversionRate {
rate: number;
fromCurrency: string;
toCurrency: string;
}
function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
return amount * rate.rate;
}
const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD, ${amountInEUR} EUR'ya eşittir`); // Çıktı: 100 USD, 85 EUR'ya eşittir
Tarih Biçimlendirme
interface DateFormatOptions {
locale: string;
options: Intl.DateTimeFormatOptions;
}
function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
return date.toLocaleDateString(format.locale, format.options);
}
const currentDate = new Date();
const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };
console.log("ABD Tarihi: " + formatDate(currentDate, usDateFormat));
console.log("Alman Tarihi: " + formatDate(currentDate, germanDateFormat));
console.log("Japon Tarihi: " + formatDate(currentDate, japaneseDateFormat));
Çeviri Servisi
interface Translation {
[key: string]: string; // Dinamik dil anahtarlarına izin verir
}
interface LanguageData<T extends Translation> {
languageCode: string;
translations: T;
}
const englishTranslations: Translation = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our website!"
};
const spanishTranslations: Translation = {
"hello": "Hola",
"goodbye": "Adiós",
"welcome": "¡Bienvenido a nuestro sitio web!"
};
const frenchTranslations: Translation = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre site web !"
};
const languageData: LanguageData<typeof englishTranslations>[] = [
{languageCode: "en", translations: englishTranslations },
{languageCode: "es", translations: spanishTranslations },
{languageCode: "fr", translations: frenchTranslations}
];
function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
const lang = languageData.find(lang => lang.languageCode === languageCode);
if (!lang) {
return `${languageCode} dilinde ${key} için çeviri bulunamadı.`;
}
return lang.translations[key] || `${key} için çeviri bulunamadı.`;
}
console.log(translate("hello", "en", languageData)); // Çıktı: Hello
console.log(translate("hello", "es", languageData)); // Çıktı: Hola
console.log(translate("welcome", "fr", languageData)); // Çıktı: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Çıktı: de dilinde missingKey için çeviri bulunamadı.
Sonuç
TypeScript jenerikleri, karmaşık veri tipleriyle çalışabilen yeniden kullanılabilir, tür güvenli kod yazmak için güçlü bir araçtır. Jeneriklerin temel sözdizimini, gelişmiş özelliklerini ve en iyi uygulamalarını anlayarak, TypeScript uygulamalarınızın kalitesini ve sürdürülebilirliğini önemli ölçüde artırabilirsiniz. Küresel bir kitle için uygulama geliştirirken, jenerikler çeşitli veri formatlarını ve kültürel gelenekleri ele almanıza yardımcı olarak herkes için sorunsuz bir kullanıcı deneyimi sağlayabilir.